Each template requires certain operations to be provided by the types it is instantiated with. Before C++20, the only way to describe those
requirements was through documentation. Concepts, introduced in C++20, provide a way to express requirements in a way that can be checked by the
compiler.
This improves the readability and maintainability of the code, most notably:
- It makes it clear from the declaration what types are accepted and what operations they should support. This benefit is even higher when the
concepts used to constrain the code are well-known, such as concepts defined in the standard library.
- Errors from incorrect instantiations point at the call site (code produced by the programmer), and not at some obscure details in the middle of
the implementation of an algorithm.
Should all template code be exhaustively constrained? Probably not, especially if that would lead to defining single-use concepts. But in the case
of templates designed to work with standard-style iterators, there is no good reason not to use the standard library concepts describing them. Even
adding a simple set of basic constraints, such as the required category of iterators, without covering all the operations needed for the algorithm, is
already providing value.
This rule raises an issue for generic iterator-pair algorithms that are not constrained.
Noncompliant code example
template<typename Iter, typename Func>
void adjacent_for_each(Iter first, Iter last, Func func) {
auto prev = first;
for (++first; first != last; ++first) {
func(*prev, *first);
prev = first;
}
}
Compliant solution
template<std::forward_iterator Iter, typename Func>
// Compliant, even though this template needs additional operations, for instance:
// requires std::invocable<Func&, std::iter_reference_t<Iter>, std::iter_reference_t<Iter>>
void adjacent_for_each(Iter first, Iter last, Func func) {
auto prev = first;
for (++first; first != last; ++first) {
func(*prev, *first);
prev = first;
}
}